package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.common.Sleeper;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import com.google.common.collect.ImmutableList;

public class FileMovingWatcher {

  public static final Logger logger = Logger.getLogger(FileMovingWatcher.class.getSimpleName());

  private final long waitWhileEmptyMillis;
  private final long sleepMillis;
  private final Sleeper sleeper;
  private final Path captureDir;
  private final Path targetDir;

  public FileMovingWatcher(long waitWhileEmptyMillis, long sleepMillis, Sleeper sleeper, Path captureDir, Path targetDir) {
    this.waitWhileEmptyMillis = waitWhileEmptyMillis;
    this.sleepMillis = sleepMillis;
    this.sleeper = sleeper;
    this.captureDir = captureDir;
    this.targetDir = targetDir;
  }

  public FileContentAndTargetFile latestFileContentAndTargetFile(
    String prefix, String suffix, boolean throwOnMultipleFiles) {
    List<FileContentAndTargetFile> a = allFilesIncludingLatest(
      prefix, suffix, CommonThings.noopRunnable);
    if (throwOnMultipleFiles & a.size() != 1) {
      throw new MyRuntimeException("Expected exactly one file but got: " + a);
    }
    return CommonThings.last(a);
  }

  public List<Path> moveAllFilesFromCaptureToTarget(String prefix, String suffix) {
    Path[] paths = CommonThings.listDir(captureDir);
    if (paths == null) {
      return ImmutableList.<Path>of();
    }
    for (Path path : paths) {
      Path targetFile = targetDir.resolve(prefix + path.getFileName() + suffix);
      try {
        Files.move(path, targetFile);
      } catch (IOException e) {
        throw new MyRuntimeException(String.format(
          Locale.ROOT, "Could not move %s to %s.", path, targetFile), e);
      }
    }
    return Collections.unmodifiableList(Arrays.asList(paths));
  }

  /**
   * @return never null
   */
  public List<FileContentAndTargetFile> allFilesIncludingLatest(
    String prefix, String suffix, Runnable noFilesAction) {
    Path[] paths;
    for (paths = CommonThings.listDir(captureDir); paths.length == 0; paths = CommonThings.listDir(captureDir)) {
      noFilesAction.run();
      CommonThings.sleep(sleepMillis, sleeper);
    }
    List<byte[]> fileContents = new ArrayList<>(paths.length);
    for (Path path : paths) {
      try {
        fileContents.add(CommonThings.readToByteArrayWithRetryOrNull(
          path, (sleepMillis + 1) / 2, (int) Math.ceil(waitWhileEmptyMillis / ((sleepMillis + 1) / 2)),
          sleeper));
      } catch (InterruptedException e1) {
        throw new MyRuntimeException(e1.getMessage(), e1);
      }
    }
    List<FileContentAndTargetFile> result = new ArrayList<>(paths.length);
    for (int i = 0; i < paths.length; ++i) {
      Path path = paths[i];
      Path targetFile = targetDir.resolve(prefix + path.getFileName() + suffix);
      byte[] fileContent = fileContents.get(i);
      try {
        Files.write(targetFile, fileContent);
      } catch (IOException e) {
        throw new MyRuntimeException("Could not copy file " + path + " because of: " + e, e);
      }
      try {
        Files.delete(path);
      } catch (IOException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
      result.add(new FileContentAndTargetFile(fileContent, targetFile));
    }
    return result;
  }

  public static class FileContentAndTargetFile {
    public final byte[] fileContent;
    public final Path targetFile;

    public FileContentAndTargetFile(byte[] fileContent, Path targetFile) {
      this.fileContent = fileContent;
      this.targetFile = targetFile;
    }

    @Override
    public String toString() {
      return new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).setExcludeFieldNames(
        "fileContent").append("fileContent.length", fileContent.length).build();
    }

    public byte[] fileContent() {
      return fileContent;
    }

    public Path targetFile() {
      return targetFile;
    }
  }
}
